package lsfusion.gwt.client.base.view;

import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A collection of {@link ResizableWidget} that periodically checks the outer
 * dimensions of a widget and redraws it as necessary. Every
 * {@link ResizableWidgetCollection} uses a timer, so consider the cost when
 * adding one.
 *
 * Typically, a {@link ResizableWidgetCollection} is only needed if you expect
 * your widgets to resize based on window resizing or other events. Fixed sized
 * Widgets do not need to be added to a {@link ResizableWidgetCollection} as
 * they cannot be resized.
 */
public class ResizableWidgetCollection implements ResizeHandler,
        Iterable<ResizableWidget> {
    @Override
    public void onResize(ResizeEvent event) {
        checkWidgetSize();
    }

    /**
     * Information about a widgets size.
     */
    static class ResizableWidgetInfo {

        private ResizableWidget widget;
        private int curOffsetHeight = 0;
        private int curOffsetWidth = 0;
        private int curClientHeight = 0;
        private int curClientWidth = 0;

        /**
         * Constructor.
         *
         * @param widget the widget that will be monitored
         */
        public ResizableWidgetInfo(ResizableWidget widget) {
            this.widget = widget;
            updateSizes();
        }

        public int getClientHeight() {
            return curClientHeight;
        }

        public int getClientWidth() {
            return curClientWidth;
        }

        public int getOffsetHeight() {
            return curOffsetHeight;
        }

        public int getOffsetWidth() {
            return curOffsetWidth;
        }

        /**
         * Update the current sizes.
         *
         * @return true if the sizes changed, false if not.
         */
        public boolean updateSizes() {
            int offsetWidth = widget.getElement().getOffsetWidth();
            int offsetHeight = widget.getElement().getOffsetHeight();
            int clientWidth = widget.getElement().getClientWidth();
            int clientHeight = widget.getElement().getClientHeight();
            if (offsetWidth != curOffsetWidth || offsetHeight != curOffsetHeight
                    || clientWidth != curClientWidth || clientHeight != curClientHeight) {
                this.curOffsetWidth = offsetWidth;
                this.curOffsetHeight = offsetHeight;
                this.curClientWidth = clientWidth;
                this.curClientHeight = clientHeight;
                return true;
            }

            return false;
        }
    }

    /**
     * The default delay between resize checks in milliseconds.
     */
    private static final int DEFAULT_RESIZE_CHECK_DELAY = 400;

    /**
     * A static {@link ResizableWidgetCollection} that can be used in most cases.
     */
    private static ResizableWidgetCollection staticCollection = null;

    /**
     * Get the globally accessible {@link ResizableWidgetCollection}. In most
     * cases, the global collection can be used for all {@link ResizableWidget}s.
     *
     * @return the global {@link ResizableWidgetCollection}
     */
    public static ResizableWidgetCollection get() {
        if (staticCollection == null) {
            staticCollection = new ResizableWidgetCollection();
        }
        return staticCollection;
    }

    /**
     * The timer used to periodically compare the dimensions of elements to their
     * old dimensions.
     */
    private Timer resizeCheckTimer = new Timer() {
        @Override
        public void run() {
            // Ignore changes that result from window resize events
            if (windowHeight != Window.getClientHeight()
                    || windowWidth != Window.getClientWidth()) {
                windowHeight = Window.getClientHeight();
                windowWidth = Window.getClientWidth();
                schedule(resizeCheckDelay);
                return;
            }

            // Look for elements that have new dimensions
            checkWidgetSize();

            // Start checking again
            if (resizeCheckingEnabled) {
                schedule(resizeCheckDelay);
            }
        }
    };

    /**
     * A hash map of the resizable widgets this collection is checking.
     */
    private Map<ResizableWidget, ResizableWidgetInfo> widgets = new HashMap<>();

    /**
     * The current window height.
     */
    private int windowHeight = 0;

    /**
     * The current window width.
     */
    private int windowWidth = 0;

    /**
     * The hook used to remove the window handler.
     */
    private HandlerRegistration windowHandler;

    /**
     * The delay between resize checks.
     */
    private int resizeCheckDelay = DEFAULT_RESIZE_CHECK_DELAY;

    /**
     * A boolean indicating that resize checking should run.
     */
    private boolean resizeCheckingEnabled;

    /**
     * Create a ResizableWidget.
     */
    public ResizableWidgetCollection() {
        this(DEFAULT_RESIZE_CHECK_DELAY);
    }

    /**
     * Constructor.
     *
     * @param resizeCheckingEnabled false to disable resize checking
     */
    public ResizableWidgetCollection(boolean resizeCheckingEnabled) {
        this(DEFAULT_RESIZE_CHECK_DELAY, resizeCheckingEnabled);
    }

    /**
     * Constructor.
     *
     * @param resizeCheckDelay the delay between checks in milliseconds
     */
    public ResizableWidgetCollection(int resizeCheckDelay) {
        this(resizeCheckDelay, true);
    }

    /**
     * Constructor.
     */
    protected ResizableWidgetCollection(int resizeCheckDelay,
                                        boolean resizeCheckingEnabled) {
        setResizeCheckDelay(resizeCheckDelay);
        setResizeCheckingEnabled(resizeCheckingEnabled);
    }

    /**
     * Add a resizable widget to the collection.
     *
     * @param widget the resizable widget to add
     */
    public void add(ResizableWidget widget) {
        widgets.put(widget, new ResizableWidgetInfo(widget));
    }

    /**
     * Check to see if any Widgets have been resized and call their handlers
     * appropriately.
     */
    public void checkWidgetSize() {
        for (Map.Entry<ResizableWidget, ResizableWidgetInfo> entry : widgets.entrySet()) {
            ResizableWidget widget = entry.getKey();
            ResizableWidgetInfo info = entry.getValue();

            // Call the onResize method only if the widget is attached
            if (info.updateSizes()) {
                // Check that the offset width and height are greater than 0.
                if (info.getOffsetWidth() > 0 && info.getOffsetHeight() > 0
                        && widget.isAttached()) {
                    // Send the client dimensions, which is the space available for
                    // rendering.
                    widget.onResize(info.getOffsetWidth(), info.getOffsetHeight());
                }
            }
        }
    }

    /**
     * Get the delay between resize checks in milliseconds.
     *
     * @return the resize check delay
     */
    public int getResizeCheckDelay() {
        return resizeCheckDelay;
    }

    /**
     * Check whether or not resize checking is enabled.
     *
     * @return true is resize checking is enabled
     */
    public boolean isResizeCheckingEnabled() {
        return resizeCheckingEnabled;
    }

    public Iterator<ResizableWidget> iterator() {
        return widgets.keySet().iterator();
    }

    /**
     * Remove a {@link ResizableWidget} from the collection.
     *
     * @param widget the widget to remove
     */
    public void remove(ResizableWidget widget) {
        widgets.remove(widget);
    }

    /**
     * Set the delay between resize checks in milliseconds.
     *
     * @param resizeCheckDelay the new delay
     */
    public void setResizeCheckDelay(int resizeCheckDelay) {
        this.resizeCheckDelay = resizeCheckDelay;
    }

    /**
     * Set whether or not resize checking is enabled. If disabled, elements will
     * still be resized on window events, but the timer will not check their
     * dimensions periodically.
     *
     * @param enabled true to enable the resize checking timer
     */
    public void setResizeCheckingEnabled(boolean enabled) {
        if (enabled && !resizeCheckingEnabled) {
            resizeCheckingEnabled = true;
            if (windowHandler == null) {
                windowHandler = Window.addResizeHandler(new ResizeHandler() {
                    public void onResize(ResizeEvent event) {
                        ResizableWidgetCollection.this.onResize(event);
                    }
                });
            }
            resizeCheckTimer.schedule(resizeCheckDelay);
        } else if (!enabled && resizeCheckingEnabled) {
            resizeCheckingEnabled = false;
            if (windowHandler != null) {
                windowHandler.removeHandler();
                windowHandler = null;
            }
            resizeCheckTimer.cancel();
        }
    }

    /**
     * Inform the {@link ResizableWidgetCollection} that the size of a widget has
     * changed and already been redrawn. This will prevent the widget from being
     * redrawn on the next loop.
     *
     * @param widget the widget's size that changed
     */
    public void updateWidgetSize(ResizableWidget widget) {
        if (!widget.isAttached()) {
            return;
        }

        ResizableWidgetInfo info = widgets.get(widget);
        if (info != null) {
            info.updateSizes();
        }
    }

}
